Software testing may involve coding drivers and stubs, which represents a particular development overhead as these pieces of software code are developed for unit testing of individual modules, but are subsequently discarded during module integration. However, testing of individual modules cannot be postponed until the integration testing phase. Software bugs detected during integration may necessitate several days of development effort to be undone, thus causing considerable “re-work” and, consequently, escalated project costs, and delayed delivery.
A different approach recommended by some practitioners is to follow “bottom-up” development. That is, a module M is developed before developing the modules that invoke M. This approach does eliminate the need for stubs, but following such a scheme is often not practical in large projects. On one hand, spreading the development of different module layers (often representing distinct functionalities) across different development teams, working simultaneously, is often cost-effective. On the other, since development proceeds upwards in a bottom-up approach, even a prototype of the complete application does not exist until the last module is added. This is not a situation most development teams find satisfactory.
A further approach, and the one commonly followed in practice is to create “rudimentary” drivers and stubs that have a minimum of structure and function. For example, a driver for a module M is often just a “main” program that passes particular data to M and prints relevant results. A stub is typically a dummy sub-program that may do some minimal data manipulation (for example, some input parameter to output value mapping), print verification of entry, and return. Tools may be used to generate mapping tables and wrapper code for stubs and drivers to assist in this process.
As an example, consider a program that generates a stub based on the syntactic interface of the corresponding module. The developer completes a table/vector with input values and expected return values. The stub code, when invoked, searches through the table/vector to match input parameters to records in the table and extracts the corresponding return value. Table 1 below provides an example table that may be developed from such a program. In this example, a class “calc” has a function called “divide”.
TABLE 1int divide( int numerator, int denominator) throws ExceptionCalcDivideNumeratordouble{20}20Denominatordouble{4}0divide_retdouble{5}ExceptionThrowabledividebyZero
Similarly, driver generation tools provide the syntactic wrapper code to test a function. The developer needs to write the code to set the state of the object and then invoke the function under test with correct parameters.
Table 2 below presents an example of a driver generated for testing the same function (“divide”) described with reference to Table 1 above, in which the code written by the developer is underlined.
TABLE 2Calc c = null;void setup( ) { //user needs to set state of object - this function //is automatically invoked before testDivide called c = new Calc( ); }int testDivide(int numerator, int denominator) { //user needs to fill in code to invoke function - e.g. int i = 10; int j = 2; System.out.println(c.divide(i,j)); }
The approaches briefly described above reduce the development overhead associated with producing stubs and drivers, but the resulting unit testing often remains far from comprehensive.
There is thus a need for an improved method of unit testing of software modules that seeks to bridge the gulf between unit and integration testing.